import copy

import numpy as np

import routs
import ship
import states
import decisions


class RequestForRouting:
    """
    Request for routing a transport ship.

    Attributes:
        name: Ship name.
        time: Date when the ship will be ready for routing.
    """
    name: str
    node_start: routs.Node
    node_end: routs.Node
    time: np.datetime64

    def __init__(self, name: str, node_start: routs.Node, node_end: routs.Node, time: np.datetime64):
        self.name = name
        self.node_start = node_start
        self.node_end = node_end
        self.time = time

    def __str__(self):
        return f"{self.name}: {self.node_start.name} --> {self.node_end.name} ({self.time})"


class Simulator:
    """
    Predicts the whole fleet state.
    """
    requests: list[RequestForRouting]
    ship_speeds: dict[str, ship.ShipSpeed]

    def __init__(self, requests: list[RequestForRouting], ship_speeds: dict[str, ship.ShipSpeed]):
        self.requests = requests
        self.ship_speeds = ship_speeds

        # Set the time step size.
        self.dtime = np.timedelta64(1, "h")

    def compute(self, time0: np.datetime64, state0: states.FleetState,
                programs: dict[str, decisions.Program]) -> tuple[np.datetime64, states.FleetState]:
        """
        Predict the state based on a set of individual programs.
        """

        # Initialize the time.
        time = time0

        # Initialize the state.
        state = copy.deepcopy(state0)

        # While there are unimplemented decisions.
        while programs:
            # Do one step.
            time, state, programs = self.step(time, state, programs)

        return time, state

    def step(self, time: np.datetime64, state: states.FleetState, programs: dict[str, decisions.Program]
             ) -> tuple[np.datetime64, states.FleetState, dict[str, decisions.Program]]:
        """
        Perform one step of the prediction.
        """

        # Iterate over the requests for routing.
        for request in self.requests:
            # If the start time is now.
            if request.time == time:
                # Update the ship state.
                state.transports[request.name].ready = True

        # Iterate over the ships.
        for name_ship, program_ship in programs.items():
            # If this is an icebreaker.
            if name_ship in state.icebreakers.keys():
                # Implement the decision.
                implemented, state = self.implement_decision_icebreaker(time, state, name_ship,
                                                                        program_ship.decisions[0])
            # If this is a transport ship.
            elif name_ship in state.transports.keys():
                # Implement the decision.
                implemented, state = self.implement_decision_transport(time, state, name_ship,
                                                                       program_ship.decisions[0])
            else:
                raise RuntimeError("unknown ship")

            # If the decision is fully implemented.
            if implemented:
                program_ship.pop_front()

        # Delete empty program.
        for name_ship, program_ship in programs.copy().items():
            if not program_ship.decisions:
                del programs[name_ship]

        # Update the time.
        time += self.dtime

        return time, state, programs

    def implement_decision_icebreaker(self, time: np.datetime64, state: states.FleetState, name_icebreaker: str,
                                      decision: decisions.Decision) -> tuple[bool, states.FleetState]:
        """
        Implement a decision for an icebreker.

        Args:
            time: Current time.
            state: Current state.
            decision: Decision.
            name_icebreaker: Name of the icebreaker to which the decision applies.

        Returns:
            A tuple consisting of: the next time, the next state and a boolean value that indicates
            whether the decision is fully implemented.
        """

        # Find the icebreaker's state.
        state_icebreaker = state.icebreakers[name_icebreaker]

        # Find the states of the attached ships.
        state_ships = dict()
        for name in state_icebreaker.ships:
            state_ships[name] = state.transports[name]

        # Set the maximum number of ships in a caravan.
        max_ships = 3

        # Initialize the decision status.
        implemented = False

        # If we decide to move along some edge.
        if isinstance(decision, decisions.Move):
            # If the icebreaker is not moving.
            if state_icebreaker.edge is None:
                # If we decide to move from the current node.
                if state_icebreaker.node == decision.edge.node_start:
                    # Begin the motion along the edge.
                    state_icebreaker.node = None
                    state_icebreaker.edge = decision.edge
                    state_icebreaker.passed = 0.0
                    for state_ship in state_ships.values():
                        state_ship.node = state_icebreaker.node
                        state_ship.edge = state_icebreaker.edge
                        state_ship.passed = state_icebreaker.passed
                else:
                    raise ValueError("unfeasible program")
            else:
                # If we decide to move forward.
                if state_icebreaker.edge.node_start == decision.edge.node_start:
                    pass
                # If we decide to move back.
                elif state_icebreaker.edge.node_start == decision.edge.node_end:
                    # Change the course.
                    state_icebreaker.edge = decision.edge
                    state_icebreaker.passed = 1.0 - state_icebreaker.passed
                    for state_ship in state_ships.values():
                        state_ship.edge = state_icebreaker.edge
                        state_ship.passed = state_icebreaker.passed
                else:
                    raise ValueError("unfeasible program")

            # Update the position on the edge.
            speed = self.compute_caravan_speed(time, {name_icebreaker: state_icebreaker} | state_ships)
            if speed == 0.0:
                raise ValueError("unfeasible program")
            length = state_icebreaker.edge.length * state_icebreaker.passed
            length += speed * self.dtime.astype(dtype=float)
            passed = length / state_icebreaker.edge.length
            if passed >= 1.0:
                # Update the state.
                state_icebreaker.node = state_icebreaker.edge.node_end
                state_icebreaker.edge = None
                state_icebreaker.passed = 0.0
                for state_ship in state_ships.values():
                    state_ship.node = state_icebreaker.node
                    state_ship.edge = state_icebreaker.edge
                    state_ship.passed = state_icebreaker.passed

                # Update the decision status.
                implemented = True
            else:
                # Update the state.
                state_icebreaker.passed = passed
                for state_ship in state_ships.values():
                    state_ship.passed = state_icebreaker.passed
        # If we decide to attach some ship.
        elif isinstance(decision, decisions.Attach):
            # Find the request for this ship.
            for request in self.requests:
                if request.name == decision.name:
                    break
            else:
                raise ValueError("unfeasible program")

            # Check that the ship will be available at this node.
            if state_icebreaker.node != request.node_start:
                raise ValueError("unfeasible program")

            # Check that the maximum number of ships will not be exceeded.
            if len(state_icebreaker.ships) == max_ships:
                raise ValueError("unfeasible program")

            # If the ship is ready.
            if request.time <= time:
                # Attach the ship.
                state_icebreaker.ships.append(request.name)
                state.transports[request.name].icebreaker = name_icebreaker

                # Update the decision status.
                implemented = True
        # If we decide to attach some ship.
        elif isinstance(decision, decisions.Detach):
            # Find this ship between the attached.
            for name_ship in state_icebreaker.ships:
                if name_ship == decision.name:
                    # Detach the ship.
                    state_icebreaker.ships.remove(name_ship)
                    state.transports[name_ship].icebreaker = None

                    # Update the decision status.
                    implemented = True
                    break
            else:
                raise ValueError("unfeasible program")
        else:
            raise ValueError("unsupported decision for icebreaker")

        return implemented, state

    def implement_decision_transport(self, time: np.datetime64, state: states.FleetState, name_transport: str,
                                     decision: decisions.Decision) -> tuple[bool, states.FleetState]:
        """
        Implement a decision for an icebreker.

        Args:
            time: Current time.
            state: Current state.
            decision: Decision.
            name_transport: Name of the transport ship to which the decision applies.

        Returns:
            A tuple consisting of: the next time, the next state and a boolean value that indicates
            whether the decision is fully implemented.
        """

        # Find the state of the transport.
        state_transport = state.transports[name_transport]

        # If transport is attached.
        if state_transport.icebreaker is not None:
            raise ValueError("unfeasible program")

        # Initialize the decision status.
        implemented = False

        # If we decide to move along some edge.
        if isinstance(decision, decisions.Move):
            # If the icebreaker is not moving.
            if state_transport.edge is None:
                # If we decide to move from the current node.
                if state_transport.node == decision.edge.node_start:
                    # Begin the motion along the edge.
                    state_transport.node = None
                    state_transport.edge = decision.edge
                    state_transport.passed = 0.0
                else:
                    raise ValueError("unfeasible program")
            else:
                # If we decide to move forward.
                if state_transport.edge.node_start == decision.edge.node_start:
                    pass
                # If we decide to move back.
                elif state_transport.edge.node_start == decision.edge.node_end:
                    # Change the course.
                    state_transport.edge = decision.edge
                    state_transport.passed = 1.0 - state_transport.passed
                else:
                    raise ValueError("unfeasible program")

            # Update the position on the edge.
            speed = self.compute_caravan_speed(time, {name_transport: state_transport})
            if speed == 0.0:
                raise ValueError("unfeasible program")
            length = state_transport.edge.length * state_transport.passed
            length += speed * self.dtime.astype(dtype=float)
            passed = length / state_transport.edge.length
            if passed >= 1.0:
                # Update the state.
                state_transport.node = state_transport.edge.node_end
                state_transport.edge = None
                state_transport.passed = 0.0

                # Update the decision status.
                implemented = True
            else:
                # Update the state.
                state_transport.passed = passed
        else:
            raise ValueError("unsupported decision for transport ship")

        return implemented, state

    def compute_ship_speed(self, time: np.datetime64, state_ship: states.ShipState, name_ship: str) -> float:
        """
        Compute the maximum speed of a ship.
        """
        speed = self.ship_speeds[name_ship]
        if state_ship.node is not None:
            return 0.0
        else:
            for i in range(len(state_ship.edge.part)):
                if state_ship.passed >= state_ship.edge.part[i]:
                    return speed.compute(state_ship.edge.speed_60mw[i])
            else:
                raise RuntimeError("invalit edge")

    def compute_caravan_speed(self, time: np.datetime64, ships: dict[str, states.ShipState]) -> float:
        """
        Compute the maximum speed of a caravan.
        """
        speed_caravan = np.inf
        for name_ship, state_ship in ships.items():
            speed_ship = self.compute_ship_speed(time, state_ship, name_ship)
            if speed_ship < speed_caravan:
                speed_caravan = speed_ship
        return speed_caravan
